﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace Saxxon.LibDoomSharp
{
    public class Wad : SerializableBase
    {
        const string ERROR_INVALID_DIR_OFFSET = "Invalid WAD file: Directory offset is an unexpected value.";
        const string ERROR_INVALID_LUMP_COUNT = "Invalid WAD file: Lump count is an unexpected value.";
        const string ERROR_INVALID_WAD_ID = "Invalid WAD file: Unexpected WAD ID.";

        const int DIR_ENTRY_LENGTH = WadDirectoryEntry.ENTRY_LENGTH; // length in bytes of a directory entry
        const int HEADER_LENGTH = 12; // number of bytes before lump data
        const Int32 IWAD_MARKER = 0x44415749; // "IWAD"
        const Int32 PWAD_MARKER = 0x44415750; // "PWAD"

        /// <summary>
        /// Initialize the Wad Directory.
        /// </summary>
        protected void InitializeDirectory(byte[] data)
        {
            Lumps = new List<Lump>();

            if (data != null)
            {
                using (MemoryStream mem = new MemoryStream(data))
                {
                    DataReader reader = GetReader(mem);

                    // skip ID
                    reader.ReadInt32();

                    // get lump count
                    int lumpCount = reader.ReadInt32();

                    // get directory offset
                    int dirOffset = reader.ReadInt32();

                    // get directory
                    mem.Position = dirOffset;
                    WadDirectory dir = new WadDirectory(this);
                    dir.ReadFrom(mem, lumpCount * DIR_ENTRY_LENGTH);

                    // get each lump
                    foreach (WadDirectoryEntry dirEntry in dir)
                    {
                        if (dirEntry.Length > 0)
                        {
                            // initialize the raw data pointer
                            mem.Position = dirEntry.Offset;

                            // read raw lump data
                            Lump lump = new Lump();
                            lump.ReadFrom(mem, dirEntry.Length);
                            lump.Name = dirEntry.Name;
                            Lumps.Add(lump);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// If true, this wad is a PWAD. Otherwise, it is an IWAD.
        /// </summary>
        public bool IsPwad { get; set; }

        /// <summary>
        /// Lumps in the Wad.
        /// </summary>
        public List<Lump> Lumps { get; set; }

        /// <summary>
        /// Name of the Wad. It is not saved internally.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Read WAD data. Length is ignored.
        /// </summary>
        protected override void Read(Stream stream, int length)
        {
            // create a memory buffer for the read data
            using (MemoryStream mem = new MemoryStream())
            {
                // initialize I/O
                DataWriter writer = GetWriter(mem);
                DataReader reader = GetReader(stream);

                // read and validate ID
                int id = reader.ReadInt32();
                if (id != IWAD_MARKER && id != PWAD_MARKER)
                    throw new LibDoomSharpException(ERROR_INVALID_WAD_ID);
                writer.WriteInt32(id);
                IsPwad = (id == PWAD_MARKER);

                // read and validate lump count
                int lumpCount = reader.ReadInt32();
                if (lumpCount < 0)
                    throw new LibDoomSharpException(ERROR_INVALID_LUMP_COUNT);
                writer.WriteInt32(lumpCount);

                // read and validate directory offset
                int dirOffset = reader.ReadInt32();
                if (dirOffset < 12)
                    throw new LibDoomSharpException(ERROR_INVALID_DIR_OFFSET);
                writer.WriteInt32(dirOffset);

                // buffer lump data
                byte[] rawData = reader.ReadBytes(dirOffset - HEADER_LENGTH);
                writer.Write(rawData);

                // buffer directory data
                byte[] rawDir = reader.ReadBytes(lumpCount * DIR_ENTRY_LENGTH);
                writer.Write(rawDir);

                // finalize memory buffer
                writer.Flush();

                // initialize the directory and lumps
                InitializeDirectory(mem.ToArray());
            }
        }

        /// <summary>
        /// Read a WAD file.
        /// </summary>
        /// <param name="filename"></param>
        public override void ReadFromFile(string filename)
        {
            Name = Path.GetFileNameWithoutExtension(filename);
            base.ReadFromFile(filename);
        }

        /// <summary>
        /// Retrieve the string representation of this Wad.
        /// </summary>
        public override string ToString()
        {
            return (IsPwad ? "PWAD" : "IWAD") + ": " + (Name ?? "(no name)");
        }

        /// <summary>
        /// Write WAD data.
        /// </summary>
        protected override void Write(Stream stream)
        {
            using (MemoryStream dataStream = new MemoryStream(), dirStream = new MemoryStream())
            {
                WadDirectory dir = new WadDirectory(this);
                int offset = HEADER_LENGTH;

                // iterate through the lumps
                if (Lumps != null)
                {
                    foreach (Lump lump in Lumps)
                    {
                        // append the data
                        if (lump.Length > 0)
                            dataStream.Write(lump.Data, 0, lump.Data.Length);

                        // create the directory entry
                        WadDirectoryEntry dirEntry = new WadDirectoryEntry();
                        dirEntry.Name = lump.Name;
                        dirEntry.Offset = offset;
                        dirEntry.Length = lump.Length;
                        dir.Add(dirEntry);
                    }
                }

                // assemble directory stream
                dir.WriteTo(dirStream);

                // flush substreams
                dataStream.Flush();
                dirStream.Flush();

                // assemble final output
                DataWriter writer = GetWriter(stream);
                writer.WriteInt32(IsPwad ? PWAD_MARKER : IWAD_MARKER);
                writer.WriteInt32(dir.Count);
                writer.WriteInt32((int)dataStream.Length + 12);
                writer.Write(dataStream.ToArray());
                writer.Write(dirStream.ToArray());
                writer.Flush();
            }
        }
    }
}
